/*
 * SpeedoDyno - Speedometer and dynamometer application for Android 
 * Copyright 2013 Guilhem VALENTIN
 * 
 * This file is part of SpeedoDyno
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with SpeedoDyno. If not, see <http://www.gnu.org/licenses/>.
 */
package com.gvalentin.speedodyno.view;

import java.text.DecimalFormat;

import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RadialGradient;
import android.graphics.RectF;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

import com.gvalentin.speedodyno.data.SpeedoDynoData;
import com.gvalentin.speedodyno.util.Fonts;
import com.gvalentin.speedodyno.util.IntegerUtil;
import com.gvalentin.speedodyno.util.PreferenceUtil;

/**
 * Dynamometer view
 * @author guilhem
 */
public class Dynamometer extends View {

	private static final String TAG = Dynamometer.class.getSimpleName();

	// Margin between edges and view 
	private static final float BORDER_MARGIN = 0.02f;
	
	// Size of ticks in RIM
	private static final float RIM_TICK_TINY = 0.06f;
	private static final float RIM_TICK_SHORT = 0.06f;
	private static final float RIM_TICK_LONG = 0.09f;
	
	// Data
	private SpeedoDynoData data = null;
	
	// Position of power hand
	private float powerHandPosition = 0.0f;
	private float powerHandTarget = 0.0f;
	private float powerHandVelocity = 0.0f;
	private long lastPowerHandMoveTime = -1L;
	
	// Drawing tools
	private RectF rimRect;
	private Paint rimPaint;
	private Paint rimShadowPaint;
	private Paint scaleTinyTickPaint;
	private Paint scaleShortTickPaint;
	private Paint scaleLongTickPaint;
	private Paint scaleTextPaint;
	private RectF powerHandCenterRect;
	private Paint powerHandPaint;
	private Path powerHandPath;
	private Paint averagePowerHandPaint;
	private Path averagePowerHandPath;
	private Paint facePaint;
	private Path facePath;
	private Paint powerUnitTextPaint;
	private Paint powerTextPaint;
	private Paint backgroundPaint;

	/** holds the cached static part */	
	private Bitmap background;
	
	// Number formatting tools
	private static final DecimalFormat rounder0 = new DecimalFormat("#");

	/** Initial maximum power in scale (from preferences) */
	private int initialMaxScalePower = 400;
	
	/** Maximum power in scale */
	private int maxScalePower = 400;
	
	
	/**
	 * Constructor
	 */
	public Dynamometer(Context context) {
		super(context);
		init();
	}

	/**
	 * Constructor
	 */
	public Dynamometer(Context context, AttributeSet attrs) {
		super(context, attrs);
		init();
	}

	/**
	 * Constructor
	 */
	public Dynamometer(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		init();
	}

	
	/**
	 * Init configuration. This method must be called at activity initialization,
	 * and each time the preferences are modified.
	 */
	public void configure(SharedPreferences sharedPreferences) {
		maxScalePower = PreferenceUtil.getPreference(sharedPreferences, "pref_maxPower", 400);
		initialMaxScalePower = maxScalePower;
	}

	/** Initialize the view */
	private void init() {
		initDrawingTools();
	}

	/**
	 * Init the background of the view
	 */
	private void initDrawingTools() {
		// White bar rect
		rimRect = new RectF(BORDER_MARGIN, BORDER_MARGIN, 1.0f - BORDER_MARGIN, 1.0f - BORDER_MARGIN);

		// White rim color
		rimPaint = new Paint();
		rimPaint.setAntiAlias(true);
		rimPaint.setStyle(Paint.Style.FILL);
		rimPaint.setColor(Color.WHITE);

		// Rim shadow
		rimShadowPaint = new Paint();
		rimShadowPaint.setAntiAlias(true);
		rimShadowPaint.setShader(new RadialGradient(0.5f, 0.5f, rimRect.width() / 2.0f,
				   new int[] { 0x00000000, 0x00000000, 0x00000500, 0x80000500 },
				   new float[] { 0.0f, 0.85f, 0.85f, 1f },
				   Shader.TileMode.MIRROR));
		rimShadowPaint.setStyle(Paint.Style.FILL);
		
		// Scale paint: tiny ticks
		scaleTinyTickPaint = new Paint();
		scaleTinyTickPaint.setStyle(Paint.Style.STROKE);
		scaleTinyTickPaint.setColor(Color.BLACK);
		scaleTinyTickPaint.setStrokeWidth(0.003f);
		scaleTinyTickPaint.setAntiAlias(true);
		// Scale paint: short ticks
		scaleShortTickPaint = new Paint();
		scaleShortTickPaint.setStyle(Paint.Style.STROKE);
		scaleShortTickPaint.setColor(Color.BLACK);
		scaleShortTickPaint.setStrokeWidth(0.015f);
		scaleShortTickPaint.setAntiAlias(true);
		// Scale paint: long ticks
		scaleLongTickPaint = new Paint();
		scaleLongTickPaint.setStyle(Paint.Style.STROKE);
		scaleLongTickPaint.setColor(Color.BLACK);
		scaleLongTickPaint.setStrokeWidth(0.03f);
		scaleLongTickPaint.setAntiAlias(true);
		// Scale paint: text
		scaleTextPaint = new Paint();
		scaleTextPaint.setColor(Color.BLACK);
		scaleTextPaint.setAntiAlias(true);
		scaleTextPaint.setTextSize(0.13f);
		scaleTextPaint.setTextScaleX(0.8f);
		scaleTextPaint.setTypeface(Fonts.getFontBold(getContext()));
		scaleTextPaint.setTextAlign(Paint.Align.CENTER);
		scaleTextPaint.setLinearText(true);
	
		// Power hand center
		powerHandCenterRect = new RectF();
		float centerRadius = 0.08f;
		powerHandCenterRect = new RectF(0.5f-centerRadius, 0.5f-centerRadius, 0.5f+centerRadius, 0.5f+centerRadius);
		
		// Power hand
		powerHandPaint = new Paint();
		powerHandPaint.setStyle(Paint.Style.FILL);
		powerHandPaint.setColor(Color.RED);
		powerHandPaint.setAntiAlias(true);
		powerHandPaint.setShadowLayer(0.03f, 0, 0.005f, 0xc0000000);
		powerHandPath = new Path();
		powerHandPath.moveTo(0.5f - 0.03f, 0.6f);
		powerHandPath.lineTo(0.5f - 0.016f, BORDER_MARGIN + RIM_TICK_TINY + 0.01f);
		powerHandPath.lineTo(0.5f, BORDER_MARGIN + RIM_TICK_TINY);
		powerHandPath.lineTo(0.5f + 0.016f, BORDER_MARGIN + RIM_TICK_TINY + 0.01f);
		powerHandPath.lineTo(0.5f + 0.03f, 0.6f);
		powerHandPath.lineTo(0.5f - 0.03f, 0.6f);
		
		// Average power hand
		averagePowerHandPaint = new Paint();
		averagePowerHandPaint.setStyle(Paint.Style.FILL);	
		averagePowerHandPaint.setColor(Color.RED);
		averagePowerHandPaint.setAntiAlias(true);
		averagePowerHandPaint.setShadowLayer(0.01f, 0, 0.005f, 0xc0000000);
		averagePowerHandPath = new Path();
		averagePowerHandPath.addArc(rimRect, 265.0f, 10.0f);
		averagePowerHandPath.lineTo(0.5f, BORDER_MARGIN + RIM_TICK_TINY);
		averagePowerHandPath.close();
		
		// Face paint: black
		facePaint = new Paint();
		facePaint.setAntiAlias(true);
		facePaint.setStyle(Paint.Style.FILL);
		facePaint.setColor(Color.BLACK);
		facePaint.setShadowLayer(0.06f, 0, 0.0f, 0xff000000);
		
		// Paint for power unit
		powerUnitTextPaint = new Paint();
		powerUnitTextPaint.setColor(Color.BLACK);
		powerUnitTextPaint.setAntiAlias(true);
		powerUnitTextPaint.setTypeface(Fonts.getFontRegular(getContext()));
		powerUnitTextPaint.setTextAlign(Paint.Align.CENTER);
		powerUnitTextPaint.setTextSize(0.15f);
		powerUnitTextPaint.setLinearText(true);
		
		// Face path: trapez
		facePath = new Path();
		facePath.moveTo(0.0f,  1.0f);
		facePath.lineTo(0.3f, 0.75f);
		facePath.lineTo(0.7f, 0.75f);
		facePath.lineTo(1.0f, 1.0f);
		facePath.lineTo(0.0f, 1.0f);
		
		// Power text paint
		powerTextPaint = new Paint();
		powerTextPaint.setColor(Color.WHITE);
		powerTextPaint.setAntiAlias(true);
		powerTextPaint.setTypeface(Fonts.getFontDynamic(getContext()));
		powerTextPaint.setTextAlign(Paint.Align.CENTER);
		powerTextPaint.setTextSize(0.18f);
		powerTextPaint.setLinearText(true);
		powerTextPaint.setShadowLayer(0.015f, 0, 0, 0x80FFFFFF);
		
		// Background
		backgroundPaint = new Paint();
		backgroundPaint.setFilterBitmap(true);
	}

	/**
	 * @see android.view.View#onMeasure(int, int)
	 */
	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		Log.d(TAG, "Width spec: " + MeasureSpec.toString(widthMeasureSpec));
		Log.d(TAG, "Height spec: " + MeasureSpec.toString(heightMeasureSpec));

		// keep our custom view square, i.e. keep the width equal to the height 
		// regardless of what the MeasureSpec restrictions are
		int widthMode = MeasureSpec.getMode(widthMeasureSpec);
		int widthSize = MeasureSpec.getSize(widthMeasureSpec);
		
		int heightMode = MeasureSpec.getMode(heightMeasureSpec);
		int heightSize = MeasureSpec.getSize(heightMeasureSpec);
		
		int chosenWidth = chooseDimension(widthMode, widthSize);
		int chosenHeight = chooseDimension(heightMode, heightSize);
		
		int chosenDimension = Math.min(chosenWidth, chosenHeight);
		
		setMeasuredDimension(chosenDimension, chosenDimension);
	}
	
	/**
	 * Choose the size depending on the MeasureSpec restrictions
	 */
	private int chooseDimension(int mode, int size) {
		if (mode == MeasureSpec.AT_MOST || mode == MeasureSpec.EXACTLY) {
			return size;
		} else { // (mode == MeasureSpec.UNSPECIFIED)
			return getPreferredSize();
		} 
	}
	
	/**
	 * in case there is no size specified
	 */
	private int getPreferredSize() {
		return 150;
	}

	/**
	 * @see android.view.View#onSizeChanged(int, int, int, int)
	 */
	@Override
	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
		Log.d(TAG, "Size changed to " + w + "x" + h);
		regenerateBackground();
	}
	
	/**
	 * Regenerate the background
	 */
	private void regenerateBackground() {
		// free the old bitmap
		if (background != null) {
			background.recycle();
		}
		
		background = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
		Canvas backgroundCanvas = new Canvas(background);
		float scaleX = (float) getWidth();
		float scaleY = (float) getHeight();
		backgroundCanvas.scale(scaleX, scaleY);
		
		drawRim(backgroundCanvas);
		drawPowerScale(backgroundCanvas);
	}
	
	/**
	 * @see android.view.View#onDraw(android.graphics.Canvas)
	 */
	@Override
	protected void onDraw(Canvas canvas) {
		if (checkPowerScale()) {
			regenerateBackground();
		}
		
		drawBackground(canvas);

		// Draw internal stuff only if there is something to draw
		if (data != null) {
			// Change the 2D coordinate space from width x height to 1.0 x 1.0
			// regardless of the current custom view size
			float scaleX = (float) getWidth();
			float scaleY = (float) getHeight();
			canvas.save(Canvas.MATRIX_SAVE_FLAG);
			canvas.scale(scaleX, scaleY);
			
			drawPowerHand(canvas);
			drawAveragePowerHand(canvas);
			drawPowerText(canvas);
			
			canvas.restore();
			
			// Make power hand move
			// (will auto generate a re-draw as needed, so must be called at the end)
			if (powerHandNeedsToMove()) {
				movePowerHand();
			}
		}
	}

	/**
	 * Draw the background, by reusing pre-drawn bitmap
	 */
	private void drawBackground(Canvas canvas) {
		if (background == null) {
			Log.w(TAG, "Background not created");
		} else {
			canvas.drawBitmap(background, 0, 0, backgroundPaint);
		}
	}

	/**
	 * Draw the white rim
	 */
	private void drawRim(Canvas canvas) {
		// Draw the white rim
		canvas.drawOval(rimRect, rimPaint);
		canvas.drawOval(rimRect, rimShadowPaint);
		
		// Draw the power unit
	    float x = 0.5f;
	    float y = 0.38f;
		canvas.drawText("W", x, y, powerUnitTextPaint);

		// Draw the center of power hand
		canvas.drawOval(powerHandCenterRect, facePaint);		
		
		// Draw the bottom trapez for power text
		canvas.drawPath(facePath, facePaint);
	}

	/**
	 * Check whether the power scale is OK for current power
	 * and update the max power scale if needed.
	 * Return true if the max power scale has been modified.
	 */
	private boolean checkPowerScale() {
		// Increase max scale if needed
		boolean needUpdate = false;
		if (data != null) {
			double powerAvg = data.getPowerAverage();
			// If max power scale is too high, reset to default
			if (maxScalePower > 3*powerAvg) {
				maxScalePower = initialMaxScalePower;
			}
			// Adjust displayed max depending on average power
			while (maxScalePower < 1.5*powerAvg) {
				int step;
				if (maxScalePower < 100) {
					step = 20;
				} else if (maxScalePower < 200) {
					step = 100;
				} else if (maxScalePower < 1000) {
					step = 200;
				} else if (maxScalePower < 2000) {
					step = 1000;
				} else if (maxScalePower < 10000) {
					step = 2000;
				} else {
					step = 10000;
				}
				maxScalePower = (int) Math.floor((maxScalePower + step) / step) * step;
				needUpdate = true;
			}
		}
		return needUpdate;
	}
	
	/**
	 * Draw the scale for power
	 */
	private void drawPowerScale(Canvas canvas) {
		// The minimal step between ticks
		int step;
		if (maxScalePower <= 1000) {
			step = 10;
		} else if (maxScalePower <= 10000) {
			step = 100;
		} else {
			step = 1000;
		}
		
		// Use rotation to draw all scales
		canvas.save(Canvas.MATRIX_SAVE_FLAG);
		canvas.rotate(235.0f, 0.5f, 0.5f);
		float tickAngle = step * 250.0f / maxScalePower;
		for (int i = 0; i <= maxScalePower; i+=step) {
			float y1 = rimRect.top;
			float y_tiny = y1 + RIM_TICK_TINY; // bottom for tiny ticks
			float y_short = y1 + RIM_TICK_SHORT; // bottom for short ticks
			float y_long = y1 + RIM_TICK_LONG; // bottom for long ticks
			float y4 = y1 + RIM_TICK_LONG + 0.13f; // position for text
			
			if (i % (10*step) == 0) { // long ticks
				canvas.drawLine(0.5f, y1, 0.5f, y_long, scaleLongTickPaint);
			} else if (i % (5*step) == 0) { // short ticks
				canvas.drawLine(0.5f, y1, 0.5f, y_short, scaleShortTickPaint);
			} else { // tiny ticks
				canvas.drawLine(0.5f, y1, 0.5f, y_tiny, scaleTinyTickPaint);
			}
			
			// Draw text every X units, depending on max scale
			int textPeriod = (IntegerUtil.getFirstDigit(maxScalePower-1) == 1 ? 10 : 20)*step;
			if (i % textPeriod == 0) {
				String valueString = Integer.toString(i);
				canvas.drawText(valueString, 0.5f, y4, scaleTextPaint);
			}

			canvas.rotate(tickAngle, 0.5f, 0.5f);
		}
		canvas.restore();
	}
	
	/**
	 * Set the power hand target
	 */
	private void setPowerHandTarget() {
		powerHandTarget = (float) data.getPower();
		if (powerHandTarget < 0) {
			powerHandTarget = 0;
		}
	}

	/**
	 * Indicate whether we need to move power hand from its current position
	 */
	private boolean powerHandNeedsToMove() {
		if (data == null) {
			return false;
		}
		
		double currentPower = data.getPower();
		return Math.abs(powerHandPosition - currentPower) > 1.0d;
	}
	
	/**
	 * Move power hand
	 */
	private void movePowerHand() {
		if (! powerHandNeedsToMove()) {
			return;
		}
		if (data == null) {
			return;
		}
		
		if (lastPowerHandMoveTime != -1L) {
			long currentTime = System.currentTimeMillis();
			// Delta in seconds since last move
			float delta = (currentTime - lastPowerHandMoveTime) / 1000.0f;

			// Direction: 0 for not moving, -1 for decreasing, +1 for increasing
			int currentDirection = (powerHandVelocity == 0.0f ? 0 : (powerHandVelocity > 0 ? 1 : -1));
			int targetDirection = (powerHandTarget - powerHandPosition > 0 ? 1 : -1);
			
			// Calculate required acceleration
			float acceleration = 150.0f * (powerHandTarget - powerHandPosition) / maxScalePower;
			
			// Calculate new velocity
			powerHandVelocity += acceleration * delta;
			float maxVelocity = maxScalePower*10;
			if (powerHandVelocity < -maxVelocity) {
				powerHandVelocity = -maxVelocity;
			} else if (powerHandVelocity > maxVelocity) {
				powerHandVelocity = maxVelocity;
			}
			
			// Calculate new position
			powerHandPosition += powerHandVelocity * delta;
			if (powerHandPosition < 0) {
				powerHandPosition = 0.0f;
				powerHandVelocity = 0.0f;
			} else if (powerHandPosition > maxScalePower) {
				powerHandPosition = maxScalePower;
				powerHandVelocity = 0.0f;
			}
			
			if (Math.abs(powerHandTarget - powerHandPosition) < 1.0f) {
				// Hand closed enough to the target, stick to target
				powerHandPosition = powerHandTarget;
				powerHandVelocity = 0.0f;
				lastPowerHandMoveTime = -1L;
			} else if (currentDirection != 0 && currentDirection != targetDirection) {
				// Direction changed, reset velocity
				powerHandVelocity = 0.0f;
				lastPowerHandMoveTime = System.currentTimeMillis();
			} else {
				// Continue
				lastPowerHandMoveTime = System.currentTimeMillis();
			}
			// Redraw
			invalidate();
		} else {
			lastPowerHandMoveTime = System.currentTimeMillis();
			movePowerHand();
		}
	}
	
	/**
	 * Draw the power hand
	 */
	private void drawPowerHand(Canvas canvas) {
		float angle = (float) (235.0f + (250.0f * powerHandPosition / maxScalePower));

		canvas.save(Canvas.MATRIX_SAVE_FLAG);
		canvas.rotate(angle, 0.5f, 0.5f);
		canvas.drawPath(powerHandPath, powerHandPaint);
		canvas.restore();
	}

	/**
	 * Draw the average power hand in the scale
	 */
	private void drawAveragePowerHand(Canvas canvas) {
		double avgPower = data.getPowerAverage();
		float angle = (float) (235.0f + (250.0f * avgPower / maxScalePower));

		canvas.save(Canvas.MATRIX_SAVE_FLAG);
		canvas.rotate(angle, 0.5f, 0.5f);
		canvas.drawPath(averagePowerHandPath, averagePowerHandPaint);
		canvas.restore();
	}

	/**
	 * Draw the power text
	 */
	private void drawPowerText(Canvas canvas) {
		if (data != null) {
			String powerStr = (data.getPower() > 0 ? rounder0.format(data.getPower()) : "0");

			float x = 0.5f;
			float y = 0.95f;
			canvas.drawText(powerStr, x, y, powerTextPaint);
		}
	}
	
	/**
	 * Update data
	 */
	public void setPositionData(SpeedoDynoData data) {
		this.data = data;
		setPowerHandTarget();
		invalidate();
	}
	
	/**
	 * Action to be performed when the preferences have been changed.
	 * Reset and redraw the whole display
	 */
	public void onPreferenceChanged(SharedPreferences sharedPreferences, String key) {
		if ("pref_maxPower".equals(key)) {
			configure(sharedPreferences);
			regenerateBackground();
		}
	}
}
